跳到主要内容

Linux 中的 IO 模式

在学习 Redis 和 Nginx 时经常能看到 Linux 的 IO 模型,所以它到底是什么呢?

IO 模式是什么?

对于一次 IO 访问(以 read 举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个 read 操作发生时,它会经历两个阶段:

  1. 等待数据准备
  2. 将数据从内核拷贝到进程中

正式因为这两个阶段,Linux 系统产生了下面五种网络模式的方案。

  • 阻塞 I/O(blocking IO)
  • 非阻塞 I/O(nonblocking IO)
  • I/O 多路复用( IO multiplexing)
  • 信号驱动 I/O( signal driven IO) 基本不用
  • 异步 I/O(asynchronous IO)

阻塞 I/O 是什么?

在 Linux 中,默认情况下所有的 Socket 都是 blocking,一个典型的读操作流程大概是这样:

当用户进程调用了 Socket 调用(就是图中的 recvfrom)这个系统调用,内核(就是图中的 kernel)就开始了 IO 的第一个阶段:准备数据。

提示

recvfrom 系统调用其实就是从套接字上接收一个消息。

当内核一直等到数据准备好了,它就会将数据从 内核 中拷贝到用户内存,然后 内核 返回结果,用户进程才解除 block 的状态,重新运行起来。

因为对于网络 IO 来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的 UDP 包。这个时候 内核 就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)

所以,blocking IO 的特点就是在 IO 执行的两个阶段都被 block 了。

非阻塞 I/O 是什么?

Linux 下,可以通过设置 Socket 使其变为 non-blocking。当对一个 non-blocking socket 执行读操作时,流程是这个样子:

当用户进程发出 read 操作时,如果 内核 中的数据还没有准备好,那么它并不会 block 用户进程,而是立刻返回一个 error。

从用户进程角度讲 ,它发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。

一旦 内核 中的数据准备好了,并且又再次收到了用户进程的 system call,那么它马上就将数据拷贝到了用户内存,然后返回。

所以,nonblocking IO 的特点是用户进程需要不断的主动询问内核数据好了没有。

I/O 多路复用又是什么呢?

I/O多路复用是一种高效的I/O处理方式,它可以在一个线程中同时监听多个I/O事件。通过使用I/O多路复用,应用程序可以同时等待多个I/O操作,而不需要为每个I/O操作创建一个单独的线程或使用非阻塞I/O轮询。常见的I/O多路复用机制包括 select、poll 和 epoll。当有一个或多个I/O事件就绪时,应用程序可以选择进行相应的I/O操作,而不会被阻塞在一个单独的I/O操作上。

因此,可以说I/O多路复用是一种实现非阻塞I/O的机制。使用I/O多路复用,应用程序可以同时监听多个I/O事件,并根据事件的就绪状态来进行相应的I/O操作。这样可以提高应用程序的并发性和效率,避免了阻塞I/O和非阻塞I/O模式下的一些问题。

select/epoll 的好处就在于单个 process 就可以同时处理多个网络连接的 IO。它的基本原理就是 select,poll,epoll 这个 function 会不断的轮询所负责的所有 socket,当某个 socket 有数据到达了,就通知用户进程。

当用户进程调用了 select,那么整个进程会被 block,而同时,内核会 “监视” 所有 select 负责的 socket,当任何一个 socket 中的数据准备好了,select 就会返回。

这个时候用户进程再调用 read 操作,将数据从内核拷贝到用户进程。

所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select() 函数就可以返回。

这个图和 blocking IO 的图其实并没有太大的不同,事实上,还更差一些。

因为这里需要使用两个 system call(select 和 Socket 调用(recvfrom)),而 blocking IO 只调用了一个 system call(Socket 调用)。

但是,用 select 的优势在于它可以同时处理多个 connection。

所以,如果处理的连接数不是很高的话,使用 select/epoll 的 web server 不一定比使用 多线程 + blocking IO 的 web server 性能更好,可能延迟还更大。

select / epoll 的优势 并不是对于单个连接能处理得更快,而是在于能处理更多的连接

在 IO multiplexing Model 中,实际中,对于每一个 socket,一般都设置成为 non-blocking,但是,如上图所示,整个用户的 process 其实是一直被 block 的。只不过 process 是被 select 这个函数 block,而不是被 socket IO 给 block。

异步 I/O 是什么?

Linux 下的 asynchronous IO 其实用得很少。先看一下它的流程:

用户进程发起 read 操作之后,立刻就可以开始去做其它的事。

而另一方面,从 内核 的角度,当它受到一个 asynchronous read 之后,首先它会立刻返回,所以不会对用户进程产生任何 block。

然后,内核 会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,内核 会给用户进程发送一个 signal,告诉它 read 操作完成了。

Reference